using Lombiq.Tests.UI.Helpers;
using Lombiq.Tests.UI.Services;
using Shouldly;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;

namespace Lombiq.Tests.UI.Extensions;

[SuppressMessage(
        "Reliability",
        "CA2000:Dispose objects before losing scope",
        Justification = "Disposed by the HttpClient.")]
public static class HttpClientUITestContextExtensions
{
    public static JsonSerializerOptions JsonSerializerOptions { get; } = new(JsonSerializerDefaults.Web);

    [Obsolete("Use CreateHttpClient instead.")]
    public static HttpClient CreateClient(this UITestContext context) => context.CreateHttpClient();

    public static HttpClient CreateHttpClient(this UITestContext context) =>
        HttpClientHelper.CreateCertificateIgnoringHttpClient(context.Scope.BaseUri);

    [Obsolete("Use CreateHttpClientWithBrowserContext instead.")]
    public static HttpClient CreateClientWithBrowserContext(this UITestContext context) =>
        context.CreateHttpClientWithBrowserContext();

    public static HttpClient CreateHttpClientWithBrowserContext(this UITestContext context)
    {
        var handler = HttpClientHelper.CreateCertificateIgnoringHttpClientHandler();
        handler.CookieContainer = context.GetCookieContainer();

        return new HttpClient(handler);
    }

    /// <summary>
    /// Creates a new <see cref="HttpClient"/> and authorizes it with a Bearer token that is created based on the
    /// provided parameters.
    /// </summary>
    public static async Task<HttpClient> CreateAndAuthorizeClientAsync(
        this UITestContext context,
        string grantType = "client_credentials",
        string clientId = "UITest",
        string clientSecret = "Password",
        string userName = null,
        string password = null,
        Action<HttpClient> configureClient = null)
    {
        var parameters = new List<KeyValuePair<string, string>>
        {
            new("grant_type", grantType),
            new("client_id", clientId),
            new("client_secret", clientSecret),
        };

        if (string.Equals(grantType, "password", StringComparison.OrdinalIgnoreCase))
        {
            parameters.Add(new KeyValuePair<string, string>("username", userName));
            parameters.Add(new KeyValuePair<string, string>("password", password));
        }

        using var requestBody = new FormUrlEncodedContent(parameters);

        var client = context.CreateHttpClient();
        configureClient?.Invoke(client);

        var tokenUrl = new Uri(context.Scope.BaseUri, "connect/token");
        using var tokenResponse = await client.PostAsync(tokenUrl, requestBody, context.Configuration.TestCancellationToken);

        await tokenResponse.ThrowIfNotSuccessAsync(
            $"Failed to get token for user in {nameof(CreateAndAuthorizeClientAsync)}.", requestBody);

        var responseContent = await tokenResponse.Content.ReadAsStringAsync(context.Configuration.TestCancellationToken);
        var token = JsonNode.Parse(responseContent)?["access_token"]?.ToString();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        return client;
    }

    /// <summary>
    /// Issues a GET request to the given <paramref name="requestUri"/> using the provided <paramref name="client"/>.
    /// </summary>
    /// <returns>The response's <see cref="HttpContent"/> as a string.</returns>
    public static async Task<string> GetAndReadResponseContentAsync(
        this UITestContext context,
        HttpClient client,
        string requestUri)
    {
        var response = await client.GetAsync(requestUri, context.Configuration.TestCancellationToken);
        return await response.Content.ReadAsStringAsync(context.Configuration.TestCancellationToken);
    }

    /// <summary>
    /// Issues a GET request to the given <paramref name="requestUri"/> using the provided <paramref name="client"/>
    /// then deserializes the response content to the given <typeparamref name="T"/>.
    /// </summary>
    /// <returns>The response's <see cref="HttpContent"/> as a string.</returns>
    public static async Task<T> GetAndReadResponseContentAsync<T>(
        this UITestContext context,
        HttpClient client,
        string requestUri)
        where T : class
    {
        var content = await GetAndReadResponseContentAsync(context, client, requestUri);
        return Deserialize<T>(content);
    }

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided <paramref name="json"/> and
    /// <paramref name="client"/>.
    /// </summary>
    /// <returns>The response's <see cref="HttpContent"/> as a string.</returns>
    public static async Task<string> PostAndReadResponseContentAsync(
        this UITestContext context,
        HttpClient client,
        string requestUri,
        string json)
    {
        using var response = await PostAndGetResponseAsync(client, json, requestUri, context.Configuration.TestCancellationToken);
        return await response.Content.ReadAsStringAsync(context.Configuration.TestCancellationToken);
    }

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided <paramref name="json"/> then
    /// deserializes the response content to the given <typeparamref name="T"/>.
    /// </summary>
    /// <returns>The deserialized <typeparamref name="T"/> object.</returns>
    public static async Task<T> PostAndReadResponseContentAsync<T>(
        this UITestContext context,
        HttpClient client,
        string json,
        string requestUri)
        where T : class =>
        Deserialize<T>(await context.PostAndReadResponseContentAsync(
            client,
            requestUri,
            json));

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided
    /// <paramref name="objectToSerialize"/>, that will be serialized as JSON, then the response content is deserialized
    /// to the given <typeparamref name="T"/> and returned.
    /// </summary>
    public static async Task<T> PostAndReadResponseContentAsync<T>(
        this UITestContext context,
        HttpClient client,
        object objectToSerialize,
        string requestUri)
        where T : class =>
        Deserialize<T>(await context.PostAndReadResponseContentAsync(
            client,
            requestUri,
            Serialize(objectToSerialize)));

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided
    /// <paramref name="objectToSerialize"/> which will be serialized as json.
    /// </summary>
    /// <returns>The response's <see cref="HttpContent"/> as a string.</returns>
    public static Task<string> PostAndReadResponseContentAsync(
        this UITestContext context,
        HttpClient client,
        object objectToSerialize,
        string requestUri) =>
        PostAndReadResponseContentAsync(context, client, requestUri, Serialize(objectToSerialize));

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided
    /// <paramref name="objectToSerialize"/> which will be serialized as json.
    /// </summary>
    /// <returns>The <see cref="HttpResponseMessage"/>.</returns>
    public static Task<HttpResponseMessage> PostAndGetResponseAsync(
        this UITestContext context,
        HttpClient client,
        object objectToSerialize,
        string requestUri) =>
        PostAndGetResponseAsync(client, Serialize(objectToSerialize), requestUri, context.Configuration.TestCancellationToken);

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided <paramref name="json"/>.
    /// </summary>
    /// <returns>The <see cref="HttpResponseMessage"/>.</returns>
    public static async Task<HttpResponseMessage> PostAndGetResponseAsync(
        HttpClient client,
        string json,
        string requestUri,
        CancellationToken cancellationToken)
    {
        var stringContent = new StringContent(json, Encoding.UTF8, MediaTypeNames.Application.Json);
        var response = await client.PostAsync(requestUri, stringContent, cancellationToken);

        return response;
    }

    /// <summary>
    /// Issues a POST request to the given <paramref name="requestUri"/> using the provided
    /// <paramref name="objectToSerialize"/>, that will be serialized as JSON, then the response is checked if it's the
    /// <paramref name="expected"/> status code.
    /// </summary>
    public static async Task PostAndResponseStatusCodeShouldBeAsync(
        this UITestContext context,
        HttpClient client,
        object objectToSerialize,
        string requestUri,
        HttpStatusCode expected)
    {
        using var response = await context.PostAndGetResponseAsync(client, objectToSerialize, requestUri);
        if (response.StatusCode == expected) return;

        string additionalContext;

        try
        {
            additionalContext = await response.Content?.ReadAsStringAsync(context.Configuration.TestCancellationToken);
        }
        catch (Exception exception)
        {
            additionalContext = "Couldn't read the response body.\n" + exception;
        }

        response.StatusCode.ShouldBe(expected, additionalContext);
    }

    /// <summary>
    /// Returns the serialized object as JSON using the default <see cref="JOptions"/> settings.
    /// </summary>
    public static string Serialize(object objectToSerialize) =>
        JsonSerializer.Serialize(objectToSerialize, JsonSerializerOptions);

    /// <summary>
    /// Deserializes the provided <paramref name="content"/> to the given <typeparamref name="T"/> using the
    /// default <see cref="JOptions"/> settings.
    /// </summary>
    public static T Deserialize<T>(string content)
        where T : class =>
        JsonSerializer.Deserialize<T>(content, JsonSerializerOptions);
}
